package lumis.portal.deployment;

import lumis.portal.filesystem.PortalBaseFolder;
import lumis.portal.filesystem.PortalFile;
import lumis.portal.filesystem.PortalFilePath;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Class loader that provides access to classes and resources in all 
 * deployed JAR modules and the portal.
 * <p>
 * This implementation is used for {@link IDeploymentManager#getClassLoader()}.
 *
 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
 * @since 6.0.0
 */
class PortalClassLoader extends ClassLoader
{
	/**
	 * Stores the JAR library class loaders in use.
	 * <p>
	 * Note that although concurrent multiple reads and single update on 
	 * this map is expected, it is not expected for concurrent multiple updates
	 * due to the complexity for maintaining it and atomicity of operations.
	 * Updates are guarded by this <code>PortalClassLoader</code> instance.
	 * @since 6.0.0
	 */
	private Map<String, JarLibraryModuleClassLoader> jarLibraryClassLoaders = 
			new ConcurrentHashMap<String, JarLibraryModuleClassLoader>(16, 0.75f, 1);
	
	/**
	 * A local cache for the classes loaded by {@link #loadClass(String, boolean)}.
	 * <p>
	 * Guarded by this instance of {@link PortalClassLoader}. 
	 * @since 6.0.0
	 */
	private Map<String, Class<?>> classesLoaded = new HashMap<String, Class<?>>(1000);
	
	/**
	 * Creates a new PortalClassLoader.
	 * @since 6.0.0
	 */
	public PortalClassLoader()
	{
		// for parent classloader is used this class's classloader
		// (expected to be the classloader for portal shared resources)
		super(PortalClassLoader.class.getClassLoader());
	}

	@Override
	public URL getResource(String name)
	{
		// search for the resource in the lumisdata/def
		URL definitionResource = getDefinitionResource(name);
		if (definitionResource != null)
			return definitionResource;
		
		// search for the resource in the JAR libraries
		for (JarLibraryModuleClassLoader classLoader: jarLibraryClassLoaders.values())
		{
			URL url = classLoader.findResource(name);
			if (url != null)
				return url;
		}

		// use the parent class loader (expected to be the shared) 
		return getParent().getResource(name);
	}

	/**
	 * Returns a resource in the <code>lumisdata/def</code> folder.
	 * @param name the resource path, relative to the lumisdata/def. If it 
	 * starts with '/' will be considered as not existing (to be consistent
	 * with the standard behavior of {@link #getResource(String)}).
	 * @return the URL for the resource, or <code>null</code> if the resource
	 * does not exist.
	 * @since 6.0.0
	 */
	private URL getDefinitionResource(String name)
	{
		if (name == null || name.startsWith("/") || name.startsWith("\\"))
			return null;
		
		// if a corresponding file exists in DEF
		PortalFile resourceFile = new PortalFile(new PortalFilePath(PortalBaseFolder.DEF, name));
		if (resourceFile.exists())
		{
			// return the URL for the file
			try
			{
				return resourceFile.toURI().toURL();
			}
			catch (MalformedURLException e)
			{
				throw new IllegalArgumentException("Could not generate URL for " + resourceFile, e);
			}
		}
		else
		{
			// return null to indicate resource does not exist
			return null;
		}
	}

	@Override
	public Enumeration<URL> getResources(String name) throws IOException
	{
		Collection<URL> resources = new ArrayList<URL>();
		
		// add resource in the lumisdata/def
		URL definitionResource = getDefinitionResource(name);
		if (definitionResource != null)
			resources.add(definitionResource);
		
		// add resources in the JAR libraries
		for (JarLibraryModuleClassLoader classLoader: jarLibraryClassLoaders.values())
		{
			URL resource = classLoader.findResource(name);
			if (resource != null)
				resources.add(resource);
		}

		// add resource in the parent (expected to be the shared) class loader 
		Enumeration<URL> enumResources = getParent().getResources(name);
		while (enumResources.hasMoreElements())
			resources.add(enumResources.nextElement());
		
		return Collections.enumeration(resources);
	}

	@Override
	protected synchronized Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException
	{
		// search className in classesLoaded
		Class<?> clazz = classesLoaded.get(className);
		if (clazz != null)
			return clazz;
		
		// try to find a classLoader that already has that className loaded
		for (Iterator<JarLibraryModuleClassLoader> itClassLoaders = jarLibraryClassLoaders.values().iterator(); 
				clazz == null && itClassLoaders.hasNext(); )
		{
			JarLibraryModuleClassLoader classLoader = (JarLibraryModuleClassLoader)itClassLoaders.next();
			clazz = classLoader.findClassIfLoaded(className);
		}
		
		// search for a classLoader that is able to find the class
		for (Iterator<JarLibraryModuleClassLoader> itClassLoaders = jarLibraryClassLoaders.values().iterator(); 
				clazz == null && itClassLoaders.hasNext(); )
		{
			JarLibraryModuleClassLoader classLoader = (JarLibraryModuleClassLoader)itClassLoaders.next();
			try
			{
				clazz = classLoader.findClass(className);
			}
			catch (ClassNotFoundException e)
			{
				// ignore class not found
			}
		}

		if (clazz == null)
		{
			// use the parent class loader (expected to be the shared) 
			clazz = getParent().loadClass(className);
		}
		
		// store result in classesLoaded
		classesLoaded.put(className, clazz);
		
		return clazz;
	}
	
	/**
	 * Indicates whether the class loader for a module is currently present
	 * in this class loder.
	 * @param moduleId the module identifier.
	 * @return true if the corresponding class loader is present, false otherwise.
	 * @since 6.0.0
	 */
	boolean containsModuleClassLoader(String moduleId)
	{
		return jarLibraryClassLoaders.containsKey(moduleId);
	}
	
	/**
	 * Removes a module class loader from this class loader. The removed module
	 * class loader is also {@link JarLibraryModuleClassLoader#destroy() destroyed}.
	 * @param moduleId the module identifier.
	 * @return the removed class loader, or <code>null</code> if there was no
	 * class loader for the specified module present.
	 * @since 6.0.0
	 */
	synchronized JarLibraryModuleClassLoader removeModuleClassLoader(String moduleId)
	{
		// remove the class loader
		JarLibraryModuleClassLoader classLoader = jarLibraryClassLoaders.remove(moduleId);
		if (classLoader != null)
		{
			// destroy the class loader
			classLoader.destroy();
			
			// clear classes loaded cache
			classesLoaded.clear();
		}
		return classLoader;
	}
	
	/**
	 * Adds a module class loader in this class loader.
	 * @param moduleId the module identifier.
	 * @param moduleClassLoader the module class loader.
	 * @since 6.0.0
	 */
	synchronized void addModuleClassLoader(String moduleId, JarLibraryModuleClassLoader moduleClassLoader)
	{
		if (jarLibraryClassLoaders.containsKey(moduleId))
			throw new IllegalStateException("ClassLoader for module " + moduleId + " already exists.");
		
		// store the class loader
		jarLibraryClassLoaders.put(moduleId, moduleClassLoader);
		
		// clear classes loaded cache
		classesLoaded.clear();
	}
}
